//
// Created by yan on 2018/10/23.
//

#include <stdint.h>
#include "lzmaSdkExecutor.h"
#include "androidNdkLogcat.h"

#include "./lzma1805/C/Precomp.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "./lzma1805/C/CpuArch.h"

#include "./lzma1805/C/Alloc.h"
#include "./lzma1805/C/7zFile.h"
#include "./lzma1805/C/7zVersion.h"
#include "./lzma1805/C/LzmaDec.h"
#include "./lzma1805/C/LzmaEnc.h"

static const char * const kCantReadMessage = "Can not read input file";
static const char * const kCantWriteMessage = "Can not write output file";
static const char * const kCantAllocateMessage = "Can not allocate memory";
static const char * const kDataErrorMessage = "Data error";

static void PrintHelp(char *buffer)
{
    strcat(buffer,
           "\nLZMA-C " MY_VERSION_CPU " : " MY_COPYRIGHT_DATE "\n\n"
           "Usage:  lzma <e|d> inputFile outputFile\n"
           "  e: encode file\n"
           "  d: decode file\n");
}

static int PrintError(char *buffer, const char *message)
{
    strcat(buffer, "\nError: ");
    strcat(buffer, message);
    strcat(buffer, "\n");
    return 1;
}

static int PrintErrorNumber(char *buffer, SRes val)
{
    sprintf(buffer + strlen(buffer), "\nError code: %x\n", (unsigned)val);
    return 1;
}

static int PrintUserError(char *buffer)
{
    return PrintError(buffer, "Incorrect command");
}


#define IN_BUF_SIZE (1 << 16)
#define OUT_BUF_SIZE (1 << 16)


static SRes Decode2(CLzmaDec *state, ISeqOutStream *outStream, ISeqInStream *inStream,
                    UInt64 unpackSize)
{
    int thereIsSize = (unpackSize != (UInt64)(Int64)-1);
    Byte inBuf[IN_BUF_SIZE];
    Byte outBuf[OUT_BUF_SIZE];
    size_t inPos = 0, inSize = 0, outPos = 0;
    LzmaDec_Init(state);
    for (;;)
    {
        if (inPos == inSize)
        {
            inSize = IN_BUF_SIZE;
            RINOK(inStream->Read(inStream, inBuf, &inSize));
            inPos = 0;
        }
        {
            SRes res;
            SizeT inProcessed = inSize - inPos;
            SizeT outProcessed = OUT_BUF_SIZE - outPos;
            ELzmaFinishMode finishMode = LZMA_FINISH_ANY;
            ELzmaStatus status;
            if (thereIsSize && outProcessed > unpackSize)
            {
                outProcessed = (SizeT)unpackSize;
                finishMode = LZMA_FINISH_END;
            }

            res = LzmaDec_DecodeToBuf(state, outBuf + outPos, &outProcessed,
                                      inBuf + inPos, &inProcessed, finishMode, &status);
            inPos += inProcessed;
            outPos += outProcessed;
            unpackSize -= outProcessed;

            if (outStream)
                if (outStream->Write(outStream, outBuf, outPos) != outPos)
                    return SZ_ERROR_WRITE;

            outPos = 0;

            if (res != SZ_OK || (thereIsSize && unpackSize == 0))
                return res;

            if (inProcessed == 0 && outProcessed == 0)
            {
                if (thereIsSize || status != LZMA_STATUS_FINISHED_WITH_MARK)
                    return SZ_ERROR_DATA;
                return res;
            }
        }
    }
}


static SRes Decode(ISeqOutStream *outStream, ISeqInStream *inStream)
{
    UInt64 unpackSize;
    int i;
    SRes res = 0;

    CLzmaDec state;

    /* header: 5 bytes of LZMA properties and 8 bytes of uncompressed size */
    unsigned char header[LZMA_PROPS_SIZE + 8];

    /* Read and parse header */

    RINOK(SeqInStream_Read(inStream, header, sizeof(header)));

    unpackSize = 0;
    for (i = 0; i < 8; i++)
        unpackSize += (UInt64)header[LZMA_PROPS_SIZE + i] << (i * 8);

    LzmaDec_Construct(&state);
    RINOK(LzmaDec_Allocate(&state, header, LZMA_PROPS_SIZE, &g_Alloc));
    res = Decode2(&state, outStream, inStream, unpackSize);
    LzmaDec_Free(&state, &g_Alloc);
    return res;
}

static SRes Encode(ISeqOutStream *outStream, ISeqInStream *inStream, UInt64 fileSize, char *rs)
{
    CLzmaEncHandle enc;
    SRes res;
    CLzmaEncProps props;

    UNUSED_VAR(rs);

    enc = LzmaEnc_Create(&g_Alloc);
    if (enc == 0)
        return SZ_ERROR_MEM;

    LzmaEncProps_Init(&props);
    res = LzmaEnc_SetProps(enc, &props);

    if (res == SZ_OK)
    {
        Byte header[LZMA_PROPS_SIZE + 8];
        size_t headerSize = LZMA_PROPS_SIZE;
        int i;

        res = LzmaEnc_WriteProperties(enc, header, &headerSize);
        for (i = 0; i < 8; i++)
            header[headerSize++] = (Byte)(fileSize >> (8 * i));
        if (outStream->Write(outStream, header, headerSize) != headerSize)
            res = SZ_ERROR_WRITE;
        else
        {
            if (res == SZ_OK)
                res = LzmaEnc_Encode(enc, outStream, inStream, NULL, &g_Alloc, &g_Alloc);
        }
    }
    LzmaEnc_Destroy(enc, &g_Alloc, &g_Alloc);
    return res;
}


static int main2(int numArgs, const char *args[], char *rs)
{
    CFileSeqInStream inStream;
    CFileOutStream outStream;
    char c;
    int res;
    int encodeMode;
    Bool useOutFile = False;

    FileSeqInStream_CreateVTable(&inStream);
    File_Construct(&inStream.file);

    FileOutStream_CreateVTable(&outStream);
    File_Construct(&outStream.file);

    if (numArgs == 1)
    {
        PrintHelp(rs);
        return 0;
    }

    if (numArgs < 3 || numArgs > 4 || strlen(args[1]) != 1)
        return PrintUserError(rs);

    c = args[1][0];
    encodeMode = (c == 'e' || c == 'E');
    if (!encodeMode && c != 'd' && c != 'D')
        return PrintUserError(rs);

    {
        size_t t4 = sizeof(UInt32);
        size_t t8 = sizeof(UInt64);
        if (t4 != 4 || t8 != 8)
            return PrintError(rs, "Incorrect UInt32 or UInt64");
    }

    if (InFile_Open(&inStream.file, args[2]) != 0)
        return PrintError(rs, "Can not open input file");

    if (numArgs > 3)
    {
        useOutFile = True;
        if (OutFile_Open(&outStream.file, args[3]) != 0)
            return PrintError(rs, "Can not open output file");
    }
    else if (encodeMode)
        PrintUserError(rs);

    if (encodeMode)
    {
        UInt64 fileSize;
        File_GetLength(&inStream.file, &fileSize);
        res = Encode(&outStream.vt, &inStream.vt, fileSize, rs);
    }
    else
    {
        res = Decode(&outStream.vt, useOutFile ? &inStream.vt : NULL);
    }

    if (useOutFile)
        File_Close(&outStream.file);
    File_Close(&inStream.file);

    if (res != SZ_OK)
    {
        if (res == SZ_ERROR_MEM)
            return PrintError(rs, kCantAllocateMessage);
        else if (res == SZ_ERROR_DATA)
            return PrintError(rs, kDataErrorMessage);
        else if (res == SZ_ERROR_WRITE)
            return PrintError(rs, kCantWriteMessage);
        else if (res == SZ_ERROR_READ)
            return PrintError(rs, kCantReadMessage);
        return PrintErrorNumber(rs, res);
    }
    return 0;
}

#if 0
int MY_CDECL main(int numArgs, const char *args[])
{
    char rs[800] = { 0 };
    int res = main2(numArgs, args, rs);
    fputs(rs, stdout);
    return res;
}
#endif

void unzip(const char *srcZipFile, const char *dstFileDir, OnStateListener const listener) {
    if (NULL != listener.onStart) {
        listener.onStart(listener.env, listener.objCallback, srcZipFile);
    }

    if (NULL == srcZipFile || NULL == dstFileDir) {
        if (NULL != listener.onError) {
            listener.onError(listener.env, listener.objCallback, srcZipFile, -1);
        }
        return;
    }

    char rs[800] = { 0 };
    char *argvs[4];
    argvs[0] = NULL;
    argvs[1] = "d";
    argvs[2] = srcZipFile;
    argvs[3] = dstFileDir;

    int res = main2(4, argvs, rs);
    if (res == 0) {
        if (NULL != listener.onFinish) {
            listener.onFinish(listener.env, listener.objCallback, srcZipFile);
        }
    } else {
        if (NULL != listener.onError) {
            listener.onError(listener.env, listener.objCallback, srcZipFile, res);
        }
    }
}

void zip(const char *srcZipFile, const char *dstZipFile, OnStateListener const listener) {
    if (NULL != listener.onStart) {
        listener.onStart(listener.env, listener.objCallback, srcZipFile);
    }

    if (NULL == srcZipFile || NULL == dstZipFile) {
        if (NULL != listener.onError) {
            listener.onError(listener.env, listener.objCallback, srcZipFile, -1);
        }
        return;
    }

    char rs[800] = { 0 };
    char *argvs[4] = {NULL, "e", srcZipFile, dstZipFile};
    int res = main2(4, argvs, rs);

    if (res == 0) {
        if (NULL != listener.onFinish) {
            listener.onFinish(listener.env, listener.objCallback, srcZipFile);
        }
    } else {
        if (NULL != listener.onError) {
            listener.onError(listener.env, listener.objCallback, srcZipFile, res);
        }
    }
}